So far we have only done some basic validations using the @Required annotation. Sometimes it's convenient to write our own logic for the validation.
In appendix C we introduce the @Required annotation as a basic way to implement validation logic. In this lesson we are going to describe custom validation methods which allow us to add specific business logic to your application.
Validation alternatives
We are going to enhance your code with this logic: if the orders are not delivered yet, then the user cannot assign them to an invoice. That is, only delivered orders can be associated with an invoice.
Adding delivered property to Order
First you have to add a new property to the Order entity. The delivered property:
There is a new delivered property now which indicates the delivery state of an order. Try the new code and mark some of the existing orders as delivered.
Note:
When the application is started, the delivered column will be added to the CommercialDocument table, it will have the value null for all records. We need to update this column with the following SQL statement:
UPDATE CommercialDocument
SET delivered =false
Validating with @EntityValidator
Up to now the user can add any order to any invoice from the Invoice module, and he can associate a particular invoice with any order from the Order module. We are going to restrict this: only delivered orders are allowed to be added to an invoice.
The first alternative to implement this validation is by using an @EntityValidator. This annotation allows you to assign the desired validation logic to your entity. Let's annotate the Order entity as show in following code:
@EntityValidator(
value=DeliveredToBeInInvoiceValidator.class, // The class with the validation logic
properties= {
@PropertyValue(name="year"), // The content of these properties
@PropertyValue(name="number"), // is moved from the 'Order' entity
@PropertyValue(name="invoice"), // to the validator before
@PropertyValue(name="delivered")// executing the validation})publicclass Order extends CommercialDocument {
Every time an Order object is created or modified an object of type DeliveredToBeInInvoiceValidator is created. Then its properties year, number, invoice and delivered are initialized with the properties of the same name from the Order object. After that, the validate() method of the validator is executed. You can see the code for the validator:
packageorg.openxava.invoicing.validators;// In 'validators' package
importorg.openxava.invoicing.model.*;importorg.openxava.util.*;importorg.openxava.validators.*;publicclass DeliveredToBeInInvoiceValidator
implements IValidator {// Must implement IValidatorprivateint year;// Properties to be injected from Orderprivateint number;privateboolean delivered;private Invoice invoice;publicvoid validate(Messages errors)// The validation logicthrowsException{if(invoice == null)return;if(!delivered){
errors.add(// By adding messages to errors the validation will fail"order_must_be_delivered", // An id from i18n file
year, number);// Arguments for the message}}// Getters and setters for year, number, delivered and invoice
...
}
The validation logic is absolutely straightforward: if an invoice is present and this order is not marked as delivered we add an error message, so the validation will fail. You should add the error message in the Invoicing/i18n/Invoicing-messages_en.properties file. As shown below:
# Messages for the Invoicing applicationorder_must_be_delivered=Order {0}/{1} must be delivered in order to be added to an Invoice
Now you can try to add orders to an invoice with the application, you will see how the undelivered orders are rejected, as shown the next figure:
Your validation is implemented correctly with @EntityValidator. It's not difficult, but a little “verbose”, because you need to write a “fully featured” new class merely to add 2 lines of code logic. Let's learn other ways to do the same validation.
Validating with a JPA callback method
We're going to try another, maybe even simpler, way to do this validation, we'll transfer the validation logic from the validator class into the Order entity itself, in this case in a @PreUpdate method.
First, remove the DeliveredToBeInInvoiceValidator class from your project. Then remove the @EntityValidator annotation from your Order entity:
//@EntityValidator( // Remove the '@EntityValidator' annotation// value=DeliveredToBeInInvoiceValidator.class,// properties= {// @PropertyValue(name="year"),// @PropertyValue(name="number"),// @PropertyValue(name="invoice"),// @PropertyValue(name="delivered")// }//)publicclass Order extends CommercialDocument {
After that we're going to add the validation again, but now inside the Order class itself. Add the validate() method to your Order class:
@PreUpdate // Just before updating the databaseprivatevoid validate()throwsException{if(invoice != null&&!isDelivered()){// The validation logic// The validation exception from Bean Validationthrownew javax.validation.ValidationException(
XavaResources.getString("order_must_be_delivered",
getYear(),
getNumber()));}}
Before saving an order this validation will be executed, if it fails an ValidationException is thrown. This exception is from the Bean Validation framework, so OpenXava knows that it is a validation exception. This way with only one method within your entity you have the validation done.
Validating in the setter
Another alternative to do your validation is to put the validation logic inside the setter method. That's a simple approach. First, remove the validate() method from the Order entity, and modify the setInvoice() method in the way you see below:
publicvoid setInvoice(Invoice invoice){if(invoice != null&&!isDelivered()){// The validation logic// The validation exception from Bean Validationthrownew javax.validation.ValidationException(
XavaResources.getString("order_must_be_delivered",
getYear(),
getNumber()));}this.invoice = invoice;// The regular setter assignment}
This works exactly the same way as the two other options. This is like the @PreUpdate alternative, only that it does not depend on JPA, it's a basic Java implementation.
Validating with Bean Validation
As a last option we are going to do the shortest one: The validation logic is put into a boolean method annotated with the @AssertTrue Bean Validation annotation.
To implement this alternative first remove the validation logic from the setInvoice() method. Then add the isDeliveredToBeInInvoice() method to your Order entity:
@AssertTrue(// Before saving it asserts if this method returns true, if not it throws an exception
message="order_must_be_delivered"// Error message in case false)privateboolean isDeliveredToBeInInvoice(){return invoice == null || isDelivered();// The validation logic}
In previous forms of validation our error message was constructed using two arguments, year and number, which in our i18n file are represented by {0}/{1} respectively. For the validation case with @AssertTrue we can not pass these two arguments to construct our error message, but we can declare properties and qualified properties of the validated bean in the definition of the message. Let's see how to modify the definition of the message:
# Messages for the Invoicing applicationorder_must_be_delivered=Order {year}/{number} must be delivered in order to be added to an Invoice
OpenXava will fill {year}/{number} with values of year and number that has the Order being updated and does not fulfill the condition of validation.
This is the simplest way to validate, because the method with the validation only has to be annotated. The Bean Validation is responsible for calling this method when saving takes place, and throws the corresponding ValidationException if the validation does not succeed.
Validating on removal with @RemoveValidator
The validations we have seen until now are processed when the entity is modified, but sometimes it's useful or it's required to process the validation before the removal of the entity, and to use the validation to cancel the entity removal.
We are going to modify the application to reject the removal of an order if it has an invoice associated. To achieve this annotate your Order entity with @RemoveValidator, as show in following code:
@RemoveValidator(OrderRemoveValidator.class)// The class with the validationpublicclass Order extends CommercialDocument {
Now, before removing an order the logic in OrderRemoveValidator is executed, and if validation fails the order is not removed. Let's look at the code for the validator:
packageorg.openxava.invoicing.validators;// In 'validators' package
importorg.openxava.invoicing.model.*;importorg.openxava.util.*;importorg.openxava.validators.*;publicclass OrderRemoveValidator
implements IRemoveValidator {// Must implement IRemoveValidatorprivate Order order;publicvoid setEntity(Object entity)// The entity to remove will be injectedthrowsException// with this method before validating{this.order = (Order) entity;}publicvoid validate(Messages errors)// The validation logicthrowsException{if(order.getInvoice()!= null){// By adding messages to errors the validation// will fail and the removal will be aborted
errors.add("cannot_delete_order_with_invoice");}}}
The validation logic is in the validate() method. Before calling the entity to be validated, it is injected using setEntity(). If messages are added to the errors object the validation will fail and the entity will not be removed. You have to add the error message in the Invoicing/i18n/Invoicing-messages_en.properties file:
cannot_delete_order_with_invoice=An order with an invoice cannot be deleted
If you try to remove an order with an associated invoice now, you will get an error message and the removal will be rejected.
You can see that using an @RemoveValidator is not difficult but verbose. You have to write a full new class to add a simple if. Let's examine a briefer alternative.
Validating on removal with a JPA callback method
We're going to try another, maybe simpler, way to do this removal validation just by moving the validation logic from the validator class to the Order entity itself, in this case in a @PreRemove method.
First, remove the OrderRemoveValidator class from your project. Also remove the @RemoveValidator annotation from your Order entity:
//@RemoveValidator(OrderRemoveValidator.class) // Remove the @RemoveValidatorpublicclass Order extends CommercialDocument {
We have just removed the validation. Let's add the functionality again, but now inside the Order class itself. Add the validateOnRemove() method in your Order class:
@PreRemove // Just before removing the entityprivatevoid validateOnRemove(){if(invoice != null){// The validation logicthrownew javax.validation.ValidationException(// Throws a runtime exception
XavaResources.getString(// To get the text message"cannot_delete_order_with_invoice"));}}
This validation will be processed before the removal of an order. If it fails an ValidationException is thrown. You can throw any runtime exception in order to abort the removal. You have done the validation with a single method inside the entity.
What's the best way of validating?
You have learned several ways to do validations in your model classes. Which of them is the best one? All of them are valid options. It depends on your circumstances and personal preferences. If you have a validation that is non-trivial and reusable across your application, then to use @EntityValidator and @RemoveValidator is a good option. On the other hand, if you want to use your model classes from outside OpenXava and without JPA, then the use of validation in setters is better.
In our example we'll use the @AssertTrue for the “delivered to be in invoice” validation and @PreRemove for the removal validation, because this is the simplest procedure.
Creating your own Bean Validation annotation
The techniques in the previous section are very useful for many validations. Nevertheless, sometimes you will face some validations that are very generic and you will want to reuse them over and over again. In this case to define your own Bean Validation annotation can be a good option. Defining a Bean Validation is more verbose but usage and reuse is simple; just adding an annotation to your property or class.
We are going to learn how to create a validator from Bean Validation.
Using a Bean Validation from your entity
It is very easy. Just annotate your property, as you see in the next code:
@ISBN // This annotation indicates this property must be validated as an ISBNprivateString isbn;
By merely adding @ISBN to your property, it will be validated before the entity is saved into the database. Great! The problem is that @ISBN is not included as a built-in constraint in the Bean Validation framework. This is not a big deal. If you want an @ISBN annotation, just create it. Indeed, we are going to create the @ISBN validation annotation in this section.
First of all, let's add a new isbn property to Product. Edit your Product class and add to it the code bellow:
Try out your Product module with the browser. Yes, the isbn property is already there. Now, you can add the validation.
Defining your own ISBN annotation
Let's create the @ISBN annotation. First, create a package in your project called org.openxava.invoicing.annotations. Then follow the instructions in the next figure:
Edit the code of your recently created ISBN annotation and leave it as in the next code:
packageorg.openxava.invoicing.annotations;// In 'annotations' package
importjava.lang.annotation.*;importjavax.validation.*;importorg.openxava.invoicing.validators.*;
@Constraint(validatedBy = ISBNValidator.class)
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)public @interface ISBN {// A regular Java annotation definitionClass<?>[] groups()default{};Class<?extends Payload>[] payload()default{};String message()default"ISBN does not exist";// The message if validation fails}
As you can see, this is a regular annotation definition. The @Constraint indicates the class with the validation logic. Let's write the ISBNValidator class.
Using Apache Commons Validator to implement the validation logic
We are going to write the ISBNValidator class with the validation logic for an ISBN. Instead of writing the ISBN validation logic we'll use the Commons Validator project from Apache. Commons Validator contains validation algorithms for email addresses, dates, URLs and so on. The commons-validator.jar is included by default in OpenXava projects, so you can use it without further configuration. The code for ISBNValidator:
packageorg.openxava.invoicing.validators;// In 'validators' package
importjavax.validation.*;importorg.openxava.invoicing.annotations.*;importorg.openxava.util.*;publicclass ISBNValidator
implements ConstraintValidator<ISBN, Object>{// Must implement ConstraintValidatorprivatestatic org.apache.commons.validator.routines.ISBNValidator
validator = // From 'Commons Validator' frameworknew org.apache.commons.validator.routines.ISBNValidator();
@Overridepublicvoid initialize(ISBN isbn){}
@Overridepublicboolean isValid(Object value,
ConstraintValidatorContext context){// // Contains the validation logicif(Is.empty(value))returntrue;return validator.isValid(value.toString());// Relies on 'Commons Validator'}}
As you see, the validator class must implement ConstraintValidator from the javax.validation package. This forces your validator to implement initialize() and isValid(). The isValid() method contains the validation logic. Note that if the value to validate is empty we assume that it is valid. Validating when the value is present is the responsibility of other annotations like @Required.
In this case the validation logic is plain vanilla, because we only call the ISBN validator from the Apache Commons Validator project. @ISBN is ready to be used. Just annotate your isbn property with it. You can see how:
Now, you can test your module, and verify that the ISBN values you enter are validated correctly. Congratulations, you have written your first Bean Validation. It's not so difficult. One annotation, one class.
This @ISBN is good enough for use in real life. Nevertheless, we'll try to improve it, simply to have the chance to experiment with a few interesting possibilities.
Call to a REST web service to validate the ISBN
Though most validators have simple logic, you can create validator with complex logic if necessary. For example, in the case of our ISBN, we want, not only to verify the correct format, but also to check that a book with that ISBN actually exists. A way to do this is by using web services.
As you already know, a web service is a functionality hosted in web servers and can be called by a program. The traditional way to develop and use web services is by means of WS-* standards, like SOAP, UDDI, etc., although, the simplest way to develop services today is REST. The basic idea of REST is to use the already existing “way to work” of the internet for inter-program communication. Calling a REST service consists of using a regular web URL to get a resource from a web server; this resource is usually data in XML, HTML, JSON or any other format. In other words, the programs use the internet just as regular users with their browsers.
There are a lot of sites with SOAP and REST web services that enable us to consult a book ISBN, but usually they are not free. So, we are going to use a cheaper alternative solution, that is, to call a normal web site to do an ISBN search, and to examine the resulting page to determine if the search has succeeded. Something like a pseudo-REST web service.
To call the web page we'll use the HtmlUnit framework. Though the main goal of this framework is to create tests for your web applications, you can use it to read any web page. We'll use it because it's easier to use than other libraries used for the same purpose, as the Apache Commons HttpClient. See how easy it is to read a web page with HtmlUnit:
WebClient client = new WebClient();
HtmlPage page = (HtmlPage) client.getPage("http://www.openxava.org/");
After that, you can use the page object to manipulate the read page.
OpenXava uses HtmlUnit as an underlaying framework for testing, so it is included with OpenXava. However, by default it's not included in OpenXava applications, so you have to include it yourself in your application. To do so, copy the files cssparser.jar, htmlunit-core-js.jar, htmlunit.jar, httpclient.jar, httpcore.jar, httpmime.jar, nekothml.jar, sac.jar, xalan.jar, xercesImpl.jar, xml-apis.jar, from the OpenXava/lib folder to Invoicing/web/WEB-INF/lib folder.
Let's modify ISBNValidator to use this REST service. See the result:
packageorg.openxava.invoicing.validators;importjavax.validation.*;importorg.apache.commons.logging.*;// To use 'Log'importorg.openxava.invoicing.annotations.*;importorg.openxava.util.*;importcom.gargoylesoftware.htmlunit.*;// To use 'HtmlUnit'importcom.gargoylesoftware.htmlunit.html.*;publicclass ISBNValidator implements ConstraintValidator<ISBN, Object>{privatestatic Log log = LogFactory.getLog(ISBNValidator.class);// Instantiate 'log'privatestatic org.apache.commons.validator.routines.ISBNValidator
validator = new org.apache.commons.validator.routines.ISBNValidator();
@Overridepublicvoid initialize(ISBN isbn){}
@Overridepublicboolean isValid(Object value,
ConstraintValidatorContext context){if(Is.empty(value))returntrue;if(!validator.isValid(value.toString()))returnfalse;return isbnExists(value);// Here we do the REST call}privateboolean isbnExists(Object isbn){String failMessage ="can't be found";// Failed search message in 'bookfinder4u'try{
WebClient client = new WebClient();
HtmlPage page = (HtmlPage) client.getPage(// We call"http://www.bookfinder4u.com/" + // bookfinder4u"IsbnSearch.aspx?isbn=" + // using an URL for searching
isbn + "&mode=direct");// by ISBNreturn page.asText()// Tests if the resulting page does not contain
.indexOf(failMessage)<0;// the error message}catch(Exception ex){
log.warn("Impossible to connect to bookfinder4u" +
"to validate the ISBN. Validation fails", ex);returnfalse;// If there are errors we assume that validation fails}}}
We simply open the URL with the ISBN as the request parameter. If the resulting page does not contain the "can not be found" error message the search has been successful. The page.asText() method returns the content of the HTML page without the HTML tags, that is, it only contains the text info.
You can use this trick with any site that allows you to do searches. Thus you can virtually consult millions of web site from inside your application. In a more pure REST web service the result will be a JSON or XML document instead of HTML, but usually you will have to pay some subscription fees.
Try out your application now and you'll see that validation will fail if you enter non-existent ISBN.
Adding attributes to your annotation
It's a good idea to create a new Bean Validation annotation if you reuse the validation several times, usually across several projects. To improve the reusability you may want to parametrize the validation code. For example, for your current project to do the search in www.bookfinder4u.com for ISBN is OK, but in another project, or even in another entity of your current project, you do not want to call this particular URL. The code of the annotation has to be more flexible.
This flexibility can be achieved by attributes. For example, we can add a boolean search attribute to our ISBN annotation in order to switch on or off the internet search for validation. To implement this functionality, just add the search attribute to the ISBN annotation code:
public @interface ISBN {boolean search()defaulttrue;// To (de)activate web search on validate// ...}
This new search attribute can be read from the validator class:
Here you see the use of the initialize() method: the source annotation can be used to initialize the validator, in this case simply by storing the isbn.search() value to evaluate it in isValid().
Now you can choose whether you want to call our pseudo-REST service or skip the ISBN validation:
@ISBN(search=false)// In this case no internet search is done to validate the ISBNprivateString isbn;
Using this simple method you can add any attribute you need to add more flexibility to your ISBN annotation.
Congratulations! You have learned how to create your own Bean Validation annotation, and by the way, to use the useful HtmlUnit tool.
JUnit tests
Our goal is not to develop a huge quantity of software, but to create quality software. At the end of the day, if you create quality software you will deliver more functionality, because you will spend more time on new and exciting things and less time on debugging legions of bugs. And you know that the only way to quality is automated testing, so lets update our test code.
Testing validation for adding to a collection
Recall that we have refined the code in a way that the user cannot assign orders to an invoice if the orders are not marked as delivered yet. After that, your current testAddOrders() of InvoiceTest can fail, because it tries to add the first order, and this first order might not be marked as delivered yet.
Let's modify the test method to run correctly and also to test your new validation functionality:
publicvoid testAddOrders()throwsException{
login("admin", "admin");
assertListNotEmpty();
execute("List.orderBy", "property=number");
execute("Mode.detailAndFirst");
execute("Sections.change", "activeSection=1");
assertCollectionRowCount("orders", 0);
execute("Collection.add",
"viewObject=xava_view_section1_orders");// execute("AddToCollection.add", "row=0"); // Now we don't select randomly
checkFirstOrderWithDeliveredEquals("Yes");// Selects one delivered order
checkFirstOrderWithDeliveredEquals("No");// Selects one not delivered order
execute("AddToCollection.add");// We try to add both
assertError(// An error, because the not delivered order cannot be added"ERROR! 1 element(s) NOT added to Orders of Invoice");
assertMessage(// A confirm message, because the delivered order has been added"1 element(s) added to Orders of Invoice");
assertCollectionRowCount("orders", 1);
checkRowCollection("orders", 0);
execute("Collection.removeSelected",
"viewObject=xava_view_section1_orders");
assertCollectionRowCount("orders", 0);}
We have modified the part for selecting orders to add. Before we selected the first order, no matter if it's delivered or not. Now we select one order delivered and one order not delivered. In this way we test if the delivered one is added and the not delivered one is rejected.
The missing piece here is the way to check the orders. This is the task of the checkFirstOrderWithDeliveredEquals() method:
privatevoid checkFirstOrderWithDeliveredEquals(String value)throwsException{int c = getListRowCount();// The total displayed rows in listfor(int i=0; i<c; i++){if(value.equals(
getValueInList(i, 12)))// 12 is the 'delivered' column{
checkRow(i);return;}}
fail("There must be at least one row with delivered=" + value);}
Here you see a good technique to do a loop over the displayed list elements in order to check them, get data or do whatever you want with the list data.
Testing validation assigning a reference and validation on removal
From the Invoice module the user cannot add orders to an invoice if they are not delivered yet, therefore, from the Order module the user cannot assign an invoice to an order if the order is not delivered. That is, we have to test the other side of the association too. We'll do it by modifying the existing testSetInvoice() of OrderTest.
Moreover, we'll use this case to test the remove validation we introduced in sections Validating on removal with @RemoveValidator and Validating on removal with a JPA callback method. There we modified the application to prevent the user from removing an order which has an invoice associated with it. Now we will test this restriction.
The revision of testSetInvoice() with all these enhancements is below:
publicvoid testSetInvoice()throwsException{
login("admin", "admin");
assertListNotEmpty();
execute("List.orderBy", "property=number");// To set the list order
execute("Mode.detailAndFirst");
assertValue("delivered", "false");// The order must be not delivered
execute("Sections.change", "activeSection=1");
assertValue("invoice.number", "");
assertValue("invoice.year", "");
execute("Reference.search",
"keyProperty=invoice.year");
execute("List.orderBy", "property=number");String year = getValueInList(0, "year");String number = getValueInList(0, "number");
execute("ReferenceSearch.choose", "row=0");
assertValue("invoice.year", year);
assertValue("invoice.number", number);// Not delivered order cannot have invoice
execute("CRUD.save");
assertErrorsCount(1);// We cannot save because it is not delivered
setValue("delivered", "true");
execute("CRUD.save");// With delivered=true we can save the order
assertNoErrors();// Order with invoice cannot be deleted
execute("Mode.list");// We go to list and
execute("CRUD.deleteRow", "row=0");// we delete the saved order
assertError("Impossible to remove Order because: " + // We cannot delete because"An order with an invoice cannot be deleted");// it has an invoice associated// Restoring original values
execute("Mode.detailAndFirst");
setValue("delivered", "false");
setValue("invoice.year", "");
execute("CRUD.save");
assertNoErrors();}
The original test only searched for an invoice, but did not even save it. Now, we added test code at the end which tries to save the order with delivered=false and with delivered=true, in this way we test the validation. After that, we try to delete the order, that has an invoice. Thus we test the validation on removal too.
Testing the custom Bean Validation
The last step is to test the ISBN Bean Validation, which uses a REST service to do validation. We simply have to write a test case that tries to assign an incorrect, a nonexistent and a correct ISBN to a product and checks the results for these cases. To do so let's add a testISBNValidator() method to ProductTest.
publicvoid testISBNValidator()throwsException{
login("admin", "admin");// Searching the product1
execute("CRUD.new");
setValue("number", Integer.toString(product1.getNumber()));
execute("CRUD.refresh");
assertValue("description", "JUNIT Product 1");
assertValue("isbn", "");// With incorrect ISBN format
setValue("isbn", "1111");
execute("CRUD.save");// Fails because of format (apache commons validator)
assertError("1111 is not a valid value for ISBN of " +
"Product: ISBN does not exist");// ISBN does not exist though it has correct format
setValue("isbn", "9791034369997");
execute("CRUD.save");// Fails because it does not exist (REST service)
assertError("9791034369997 is not a valid value for ISBN of " +
"Product: ISBN does not exist");// ISBN exists
setValue("isbn", "9780932633439");
execute("CRUD.save");// It does not fail
assertNoErrors();}
Surely the manual testing you were doing during the development of the @ISBN validator was like the one above. Therefore, if you write your JUnit test before the application code, you can use it as you proceed. This is more efficient than repeating the test procedures by hand using the browser over and over again.
Note that if you use @ISBN(search=false) this test will not work because it checks the result of the REST service. So, you have to use the @ISBN annotation of the isbn property without the search attribute in order to run this test successfully.
Now execute all the tests for your Invoicing application to verify that everything works as expected.
Summary
In this lesson you have learned several ways to do validation in an OpenXava application. Also, you know how to encapsulate the reusable validation logic in annotations with custom Bean Validation.
Validation is an important part of the logic of your application, and we encourage you to put it into the model, i. e. into your entities. We demonstrated several examples for this technique in the lesson. Sometimes it is more convenient to put logic outside your model classes. You will learn that in the next lessons.
Table of Contents
Lesson 6: Advanced validation
So far we have only done some basic validations using the @Required annotation. Sometimes it's convenient to write our own logic for the validation.In appendix C we introduce the @Required annotation as a basic way to implement validation logic. In this lesson we are going to describe custom validation methods which allow us to add specific business logic to your application.
Validation alternatives
We are going to enhance your code with this logic: if the orders are not delivered yet, then the user cannot assign them to an invoice. That is, only delivered orders can be associated with an invoice.Adding delivered property to Order
First you have to add a new property to the Order entity. The delivered property:Moreover it's necessary to add the delivered property to the view. Modify the Order view as shown in the following code:
There is a new delivered property now which indicates the delivery state of an order. Try the new code and mark some of the existing orders as delivered.
When the application is started, the delivered column will be added to the CommercialDocument table, it will have the value null for all records. We need to update this column with the following SQL statement:
Validating with @EntityValidator
Up to now the user can add any order to any invoice from the Invoice module, and he can associate a particular invoice with any order from the Order module. We are going to restrict this: only delivered orders are allowed to be added to an invoice.The first alternative to implement this validation is by using an @EntityValidator. This annotation allows you to assign the desired validation logic to your entity. Let's annotate the Order entity as show in following code:
Every time an Order object is created or modified an object of type DeliveredToBeInInvoiceValidator is created. Then its properties year, number, invoice and delivered are initialized with the properties of the same name from the Order object. After that, the validate() method of the validator is executed. You can see the code for the validator:
The validation logic is absolutely straightforward: if an invoice is present and this order is not marked as delivered we add an error message, so the validation will fail. You should add the error message in the Invoicing/i18n/Invoicing-messages_en.properties file. As shown below:
Now you can try to add orders to an invoice with the application, you will see how the undelivered orders are rejected, as shown the next figure:
Your validation is implemented correctly with @EntityValidator. It's not difficult, but a little “verbose”, because you need to write a “fully featured” new class merely to add 2 lines of code logic. Let's learn other ways to do the same validation.
Validating with a JPA callback method
We're going to try another, maybe even simpler, way to do this validation, we'll transfer the validation logic from the validator class into the Order entity itself, in this case in a @PreUpdate method.First, remove the DeliveredToBeInInvoiceValidator class from your project. Then remove the @EntityValidator annotation from your Order entity:
After that we're going to add the validation again, but now inside the Order class itself. Add the validate() method to your Order class:
Before saving an order this validation will be executed, if it fails an ValidationException is thrown. This exception is from the Bean Validation framework, so OpenXava knows that it is a validation exception. This way with only one method within your entity you have the validation done.
Validating in the setter
Another alternative to do your validation is to put the validation logic inside the setter method. That's a simple approach. First, remove the validate() method from the Order entity, and modify the setInvoice() method in the way you see below:This works exactly the same way as the two other options. This is like the @PreUpdate alternative, only that it does not depend on JPA, it's a basic Java implementation.
Validating with Bean Validation
As a last option we are going to do the shortest one: The validation logic is put into a boolean method annotated with the @AssertTrue Bean Validation annotation.To implement this alternative first remove the validation logic from the setInvoice() method. Then add the isDeliveredToBeInInvoice() method to your Order entity:
In previous forms of validation our error message was constructed using two arguments, year and number, which in our i18n file are represented by {0}/{1} respectively. For the validation case with @AssertTrue we can not pass these two arguments to construct our error message, but we can declare properties and qualified properties of the validated bean in the definition of the message. Let's see how to modify the definition of the message:
OpenXava will fill {year}/{number} with values of year and number that has the Order being updated and does not fulfill the condition of validation.
This is the simplest way to validate, because the method with the validation only has to be annotated. The Bean Validation is responsible for calling this method when saving takes place, and throws the corresponding ValidationException if the validation does not succeed.
Validating on removal with @RemoveValidator
The validations we have seen until now are processed when the entity is modified, but sometimes it's useful or it's required to process the validation before the removal of the entity, and to use the validation to cancel the entity removal.We are going to modify the application to reject the removal of an order if it has an invoice associated. To achieve this annotate your Order entity with @RemoveValidator, as show in following code:
Now, before removing an order the logic in OrderRemoveValidator is executed, and if validation fails the order is not removed. Let's look at the code for the validator:
The validation logic is in the validate() method. Before calling the entity to be validated, it is injected using setEntity(). If messages are added to the errors object the validation will fail and the entity will not be removed. You have to add the error message in the Invoicing/i18n/Invoicing-messages_en.properties file:
If you try to remove an order with an associated invoice now, you will get an error message and the removal will be rejected.
You can see that using an @RemoveValidator is not difficult but verbose. You have to write a full new class to add a simple if. Let's examine a briefer alternative.
Validating on removal with a JPA callback method
We're going to try another, maybe simpler, way to do this removal validation just by moving the validation logic from the validator class to the Order entity itself, in this case in a @PreRemove method.First, remove the OrderRemoveValidator class from your project. Also remove the @RemoveValidator annotation from your Order entity:
We have just removed the validation. Let's add the functionality again, but now inside the Order class itself. Add the validateOnRemove() method in your Order class:
This validation will be processed before the removal of an order. If it fails an ValidationException is thrown. You can throw any runtime exception in order to abort the removal. You have done the validation with a single method inside the entity.
What's the best way of validating?
You have learned several ways to do validations in your model classes. Which of them is the best one? All of them are valid options. It depends on your circumstances and personal preferences. If you have a validation that is non-trivial and reusable across your application, then to use @EntityValidator and @RemoveValidator is a good option. On the other hand, if you want to use your model classes from outside OpenXava and without JPA, then the use of validation in setters is better.In our example we'll use the @AssertTrue for the “delivered to be in invoice” validation and @PreRemove for the removal validation, because this is the simplest procedure.
Creating your own Bean Validation annotation
The techniques in the previous section are very useful for many validations. Nevertheless, sometimes you will face some validations that are very generic and you will want to reuse them over and over again. In this case to define your own Bean Validation annotation can be a good option. Defining a Bean Validation is more verbose but usage and reuse is simple; just adding an annotation to your property or class.We are going to learn how to create a validator from Bean Validation.
Using a Bean Validation from your entity
It is very easy. Just annotate your property, as you see in the next code:By merely adding @ISBN to your property, it will be validated before the entity is saved into the database. Great! The problem is that @ISBN is not included as a built-in constraint in the Bean Validation framework. This is not a big deal. If you want an @ISBN annotation, just create it. Indeed, we are going to create the @ISBN validation annotation in this section.
First of all, let's add a new isbn property to Product. Edit your Product class and add to it the code bellow:
Try out your Product module with the browser. Yes, the isbn property is already there. Now, you can add the validation.
Defining your own ISBN annotation
Let's create the @ISBN annotation. First, create a package in your project called org.openxava.invoicing.annotations. Then follow the instructions in the next figure:Edit the code of your recently created ISBN annotation and leave it as in the next code:
As you can see, this is a regular annotation definition. The @Constraint indicates the class with the validation logic. Let's write the ISBNValidator class.
Using Apache Commons Validator to implement the validation logic
We are going to write the ISBNValidator class with the validation logic for an ISBN. Instead of writing the ISBN validation logic we'll use the Commons Validator project from Apache. Commons Validator contains validation algorithms for email addresses, dates, URLs and so on. The commons-validator.jar is included by default in OpenXava projects, so you can use it without further configuration. The code for ISBNValidator:As you see, the validator class must implement ConstraintValidator from the javax.validation package. This forces your validator to implement initialize() and isValid(). The isValid() method contains the validation logic. Note that if the value to validate is empty we assume that it is valid. Validating when the value is present is the responsibility of other annotations like @Required.
In this case the validation logic is plain vanilla, because we only call the ISBN validator from the Apache Commons Validator project.
@ISBN is ready to be used. Just annotate your isbn property with it. You can see how:
Now, you can test your module, and verify that the ISBN values you enter are validated correctly. Congratulations, you have written your first Bean Validation. It's not so difficult. One annotation, one class.
This @ISBN is good enough for use in real life. Nevertheless, we'll try to improve it, simply to have the chance to experiment with a few interesting possibilities.
Call to a REST web service to validate the ISBN
Though most validators have simple logic, you can create validator with complex logic if necessary. For example, in the case of our ISBN, we want, not only to verify the correct format, but also to check that a book with that ISBN actually exists. A way to do this is by using web services.As you already know, a web service is a functionality hosted in web servers and can be called by a program. The traditional way to develop and use web services is by means of WS-* standards, like SOAP, UDDI, etc., although, the simplest way to develop services today is REST. The basic idea of REST is to use the already existing “way to work” of the internet for inter-program communication. Calling a REST service consists of using a regular web URL to get a resource from a web server; this resource is usually data in XML, HTML, JSON or any other format. In other words, the programs use the internet just as regular users with their browsers.
There are a lot of sites with SOAP and REST web services that enable us to consult a book ISBN, but usually they are not free. So, we are going to use a cheaper alternative solution, that is, to call a normal web site to do an ISBN search, and to examine the resulting page to determine if the search has succeeded. Something like a pseudo-REST web service.
To call the web page we'll use the HtmlUnit framework. Though the main goal of this framework is to create tests for your web applications, you can use it to read any web page. We'll use it because it's easier to use than other libraries used for the same purpose, as the Apache Commons HttpClient. See how easy it is to read a web page with HtmlUnit:
After that, you can use the page object to manipulate the read page.
OpenXava uses HtmlUnit as an underlaying framework for testing, so it is included with OpenXava. However, by default it's not included in OpenXava applications, so you have to include it yourself in your application. To do so, copy the files cssparser.jar, htmlunit-core-js.jar, htmlunit.jar, httpclient.jar, httpcore.jar, httpmime.jar, nekothml.jar, sac.jar, xalan.jar, xercesImpl.jar, xml-apis.jar, from the OpenXava/lib folder to Invoicing/web/WEB-INF/lib folder.
Let's modify ISBNValidator to use this REST service. See the result:
We simply open the URL with the ISBN as the request parameter. If the resulting page does not contain the "can not be found" error message the search has been successful. The page.asText() method returns the content of the HTML page without the HTML tags, that is, it only contains the text info.
You can use this trick with any site that allows you to do searches. Thus you can virtually consult millions of web site from inside your application. In a more pure REST web service the result will be a JSON or XML document instead of HTML, but usually you will have to pay some subscription fees.
Try out your application now and you'll see that validation will fail if you enter non-existent ISBN.
Adding attributes to your annotation
It's a good idea to create a new Bean Validation annotation if you reuse the validation several times, usually across several projects. To improve the reusability you may want to parametrize the validation code. For example, for your current project to do the search in www.bookfinder4u.com for ISBN is OK, but in another project, or even in another entity of your current project, you do not want to call this particular URL. The code of the annotation has to be more flexible.This flexibility can be achieved by attributes. For example, we can add a boolean search attribute to our ISBN annotation in order to switch on or off the internet search for validation. To implement this functionality, just add the search attribute to the ISBN annotation code:
This new search attribute can be read from the validator class:
Here you see the use of the initialize() method: the source annotation can be used to initialize the validator, in this case simply by storing the isbn.search() value to evaluate it in isValid().
Now you can choose whether you want to call our pseudo-REST service or skip the ISBN validation:
Using this simple method you can add any attribute you need to add more flexibility to your ISBN annotation.
Congratulations! You have learned how to create your own Bean Validation annotation, and by the way, to use the useful HtmlUnit tool.
JUnit tests
Our goal is not to develop a huge quantity of software, but to create quality software. At the end of the day, if you create quality software you will deliver more functionality, because you will spend more time on new and exciting things and less time on debugging legions of bugs. And you know that the only way to quality is automated testing, so lets update our test code.Testing validation for adding to a collection
Recall that we have refined the code in a way that the user cannot assign orders to an invoice if the orders are not marked as delivered yet. After that, your current testAddOrders() of InvoiceTest can fail, because it tries to add the first order, and this first order might not be marked as delivered yet.Let's modify the test method to run correctly and also to test your new validation functionality:
We have modified the part for selecting orders to add. Before we selected the first order, no matter if it's delivered or not. Now we select one order delivered and one order not delivered. In this way we test if the delivered one is added and the not delivered one is rejected.
The missing piece here is the way to check the orders. This is the task of the checkFirstOrderWithDeliveredEquals() method:
Here you see a good technique to do a loop over the displayed list elements in order to check them, get data or do whatever you want with the list data.
Testing validation assigning a reference and validation on removal
From the Invoice module the user cannot add orders to an invoice if they are not delivered yet, therefore, from the Order module the user cannot assign an invoice to an order if the order is not delivered. That is, we have to test the other side of the association too. We'll do it by modifying the existing testSetInvoice() of OrderTest.Moreover, we'll use this case to test the remove validation we introduced in sections Validating on removal with @RemoveValidator and Validating on removal with a JPA callback method. There we modified the application to prevent the user from removing an order which has an invoice associated with it. Now we will test this restriction.
The revision of testSetInvoice() with all these enhancements is below:
The original test only searched for an invoice, but did not even save it. Now, we added test code at the end which tries to save the order with delivered=false and with delivered=true, in this way we test the validation. After that, we try to delete the order, that has an invoice. Thus we test the validation on removal too.
Testing the custom Bean Validation
The last step is to test the ISBN Bean Validation, which uses a REST service to do validation. We simply have to write a test case that tries to assign an incorrect, a nonexistent and a correct ISBN to a product and checks the results for these cases. To do so let's add a testISBNValidator() method to ProductTest.Surely the manual testing you were doing during the development of the @ISBN validator was like the one above. Therefore, if you write your JUnit test before the application code, you can use it as you proceed. This is more efficient than repeating the test procedures by hand using the browser over and over again.
Note that if you use @ISBN(search=false) this test will not work because it checks the result of the REST service. So, you have to use the @ISBN annotation of the isbn property without the search attribute in order to run this test successfully.
Now execute all the tests for your Invoicing application to verify that everything works as expected.
Summary
In this lesson you have learned several ways to do validation in an OpenXava application. Also, you know how to encapsulate the reusable validation logic in annotations with custom Bean Validation.Validation is an important part of the logic of your application, and we encourage you to put it into the model, i. e. into your entities. We demonstrated several examples for this technique in the lesson. Sometimes it is more convenient to put logic outside your model classes. You will learn that in the next lessons.
Download source code of this lesson
Any problem with this lesson? Ask in the forum Everything fine? Go to Lesson 7